﻿using GameToolkit;
using ICSharpCode.SharpZipLib.Zip;
using LitJson;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Devil.Persistent
{
    public partial class PersistentSystem
    {
        class WriteDataBlock
        {
            internal readonly IPersistentBlock block;
            internal readonly HashSet<object> datas;
            internal readonly HashSet<object> references;
            internal JsonData serializedDatas;
            internal JsonData serializedRefs;
            internal bool hasReferences;
            internal bool hasBeforeCmds;
            internal bool hasBeforeCmdsRef;
            internal bool hasBinaryFile;
            //internal List<IBeforeSerializeScheduler> beforeCmds;
            //internal List<IAfterSerializeScheduler> afterCmds;
            internal WriteDataBlock(IPersistentBlock block)
            {
                this.block = block;
                datas = new HashSet<object>();
                references = new HashSet<object>();
            }

            internal object GetInstance(int instanceId)
            {
                foreach (var obj in datas)
                {
                    if (obj is ISharedInstance inst && inst.InstanceId == instanceId)
                        return inst;
                }
                foreach (var obj in references)
                {
                    if (obj is ISharedInstance inst && inst.InstanceId == instanceId)
                        return inst;
                }
                return null;
            }
            //internal List<IBeforeSerializeScheduler> EnsureBeforeCmds()
            //{
            //    if (beforeCmds == null)
            //        beforeCmds = new List<IBeforeSerializeScheduler>();
            //    return beforeCmds;
            //}

            //internal List<IAfterSerializeScheduler> EnsureAfterCmds()
            //{
            //    if (afterCmds == null)
            //        afterCmds = new List<IAfterSerializeScheduler>();
            //    return afterCmds;
            //}
        }

        class SaveGameCmd : RwCmd, IYieldCommand, IBreakCommandSignal
        {
            PersistentSystem mSys;
            IPersistentMeta mMeta;
            string mFilePath;
            bool mHasError;
            List<WriteDataBlock> mBlocks;
            List<IBeforeSerializeScheduler> mBeforeCmds;
            Dictionary<int, IBinaryPersistentFile> mBinaryFiles;
            //List<IAfterSerializeScheduler> mAfterCmds;
            //HashSet<IReferencedPersistent> mReferenceBuffer;

            internal SaveGameCmd(PersistentSystem sys, IPersistentMeta meta, string filePath)
            {
                mSys = sys;
                mFilePath = filePath;
                mMeta = meta;
                mBlocks = new List<WriteDataBlock>();
                mBeforeCmds = new List<IBeforeSerializeScheduler>();
                mBinaryFiles = new Dictionary<int, IBinaryPersistentFile>();
                //mAfterCmds = new List<IAfterSerializeScheduler>();
                //mReferenceBuffer = new HashSet<IReferencedPersistent>();
            }

            internal override IPersistentBlock GetBlock(object instance)
            {
                for (int i = mBlocks.Count - 1; i >= 0; i--)
                {
                    if (mBlocks[i].block.IsInstanceSerializer(instance))
                        return mBlocks[i].block;
                }
                return null;
            }

            internal override object GetInstance(string blockName, int instanceId)
            {
                for (int i = mBlocks.Count - 1; i >= 0; i--)
                {
                    var block = mBlocks[i];
                    if (blockName.EqualsIgnoreCase(block.block.BlockName))
                    {
                        return block.GetInstance(instanceId);
                    }
                }
                return null;
            }

            public override string ToString()
            {
                if (mMeta == null || mMeta.BlockType == EPersistentType.System)
                    return $"Save System @{mFilePath}";
                else
                    return $"Save Game @{mFilePath}";
            }

            void NotifyResult()
            {
                mSys.mOnSaved?.Invoke(mMeta, !mHasError);
            }

            void IBreakCommandSignal.OnBreak()
            {
                mHasError = true;
                lock (mSys.mRwCmds)
                {
                    mSys.mRwCmds.Remove(this);
                }
                lock (_singletonLock)
                {
                    if (mMeta == null || mMeta.BlockType == EPersistentType.System)
                    {
                        mSys.mSystemJob = ParallelUtils.Schedule(NotifyResult, true, mSys.mSystemJob);
                    }
                    else
                    {
                        mSys.mGameJob = ParallelUtils.Schedule(NotifyResult, true, mSys.mGameJob);
                    }
                }
            }

            IEnumerator IYieldCommand.Execute()
            {
                var blockType = mMeta == null ? EPersistentType.System : mMeta.BlockType;
                foreach (var mod in mSys.mModules)
                {
                    if (mod is IPersistentBlock block && block.BlockType == blockType)
                    {
                        mBlocks.Add(new WriteDataBlock(block));
                    }
                    if (mod is IBeforeSerializeScheduler cmd1)
                        mBeforeCmds.Add(cmd1);
                    //if (mod is IAfterSerializeScheduler cmd2)
                    //    mAfterCmds.Add(cmd2);
                }
                var job = ParallelUtils.ScheduleParallel<WriteDataBlock>(GetDatas, mBlocks);
                job = ParallelUtils.Schedule(GetReferences, false, job);
                var binJob = ParallelUtils.Schedule(GetBinaryFiles, false, job);
                yield return job;

                lock (mSys.mRwCmds)
                {
                    if (!mSys.mRwCmds.Contains(this))
                        mSys.mRwCmds.Add(this);
                }

                var beforeCmds = ParallelUtils.NewCombination($"Before Serialize({blockType})");
                if (mBeforeCmds.Count > 0)
                {
                    for (int i = 0; i < mBeforeCmds.Count; i++)
                    {
                        beforeCmds.Combine(mBeforeCmds[i].Schedule(mMeta));
                    }
                }
                for (int i = 0; i < mBlocks.Count; i++)
                {
                    var block = mBlocks[i];
                    if (block.hasBeforeCmds)
                    {
                        foreach (var inst in block.datas)
                        {
                            if (inst is IBeforeSerializeScheduler ser)
                                beforeCmds.Combine(ser.Schedule(mMeta));
                        }
                    }
                    if (block.hasBeforeCmdsRef)
                    {
                        foreach (var inst in block.references)
                        {
                            if (inst is IBeforeSerializeScheduler ser)
                                beforeCmds.Combine(ser.Schedule(mMeta));
                        }
                    }
                }
                //if (beforeCmds.IsAlive)
                //    yield return beforeCmds;
                job = beforeCmds.GetHandler();
                var job3 = ParallelUtils.ScheduleParallel<WriteDataBlock>(SerializeDatas, mBlocks, job);
                var job4 = ParallelUtils.ScheduleParallel<WriteDataBlock>(SerializeReferences, mBlocks, job);
                if (job3.IsAlive)
                    yield return job3;
                if (job4.IsAlive)
                    yield return job4;
                //var afterCmds = ParallelUtils.NewCombination($"After Serialize({blockType})");
                //if (mAfterCmds.Count > 0)
                //{
                //    for (int i = 0; i < mAfterCmds.Count; i++)
                //    {
                //        afterCmds.Combine(mAfterCmds[i].Schedule(mMeta));
                //    }
                //}
                //for (int i = 0; i < mBlocks.Count; i++)
                //{
                //    var block = mBlocks[i];
                //    if (block.afterCmds != null)
                //    {
                //        for (int k = 0; k < block.afterCmds.Count; k++)
                //        {
                //            afterCmds.Combine(block.afterCmds[k].Schedule(mMeta));
                //        }
                //    }
                //}
                //if (afterCmds.IsAlive)
                //    yield return afterCmds;
                lock (mSys.mRwCmds)
                {
                    mSys.mRwCmds.Remove(this);
                }
                job = ParallelUtils.Schedule(WriteFile, false, binJob);
                yield return ParallelUtils.Schedule(NotifyResult, true, job);
            }

            private void GetDatas(WriteDataBlock block)
            {
                block.block.GatherPersistentDatas(block.datas);
                foreach (var inst in block.datas)
                {
                    block.hasBeforeCmds |= inst is IBeforeSerializeScheduler;
                    block.hasReferences |= inst is IReferencedPersistent;
                    block.hasBinaryFile |= inst is IBinaryPersistentFile;
                }
            }

            private void GetReferences()
            {
                var stack = new Stack<IReferencedPersistent>();
                var tempLst = new List<ISharedInstance>();
                var buffer = new HashSet<IReferencedPersistent>();
                for (int i = 0; i < mBlocks.Count; i++)
                {
                    var block = mBlocks[i];
                    if (!block.hasReferences)
                        continue;
                    foreach (var dt in block.datas)
                    {
                        if (dt is IReferencedPersistent refinst && buffer.Add(refinst))
                        {
                            stack.Push(refinst);
                        }
                    }
                }
                while (stack.Count > 0)
                {
                    var refinst = stack.Pop();
                    refinst.GetReferencedInstances(tempLst);
                    for (int i = 0; i < tempLst.Count; i++)
                    {
                        var obj = tempLst[i];
                        var mod = GetDataBlock(obj);
                        if (mod != null && !mod.datas.Contains(obj))
                        {
                            mod.references.Add(obj);
                            mod.hasBeforeCmdsRef |= obj is IBeforeSerializeScheduler;
                            mod.hasBinaryFile |= obj is IBinaryPersistentFile;
                        }
                        if (obj is IReferencedPersistent ref2 && buffer.Add(ref2))
                        {
                            stack.Push(ref2);
                        }
                    }
                    tempLst.Clear();
                }
            }

            void GetBinaryFiles()
            {
                mBinaryFiles.Clear();
                for (int i = 0; i < mBlocks.Count; i++)
                {
                    var block = mBlocks[i];
                    if (!block.hasBinaryFile)
                        continue;
                    foreach (var inst in block.datas)
                    {
                        if (inst is IBinaryPersistentFile f)
                            mBinaryFiles[f.InstanceId] = f;
                    }
                    foreach (var inst in block.references)
                    {
                        if (inst is IBinaryPersistentFile f)
                            mBinaryFiles[f.InstanceId] = f;
                    }
                }
            }

            private void SerializeDatas(WriteDataBlock block)
            {
                block.serializedDatas = new JsonData(JsonType.Array);
                foreach (var inst in block.datas)
                {
                    var dt = block.block.OnSerializeParallel(inst);
                    if (dt != null)
                        block.serializedDatas.Add(dt);
                }
            }

            private void SerializeReferences(WriteDataBlock block)
            {
                block.serializedRefs = new JsonData(JsonType.Array);
                foreach (var inst in block.references)
                {
                    var dt = block.block.OnSerializeParallel(inst);
                    if (dt != null)
                        block.serializedRefs.Add(dt);
                }
            }

            WriteDataBlock GetDataBlock(object inst)
            {
                if (inst == null)
                    return null;
                for (int i = 0; i < mBlocks.Count; i++)
                {
                    if (mBlocks[i].block.IsInstanceSerializer(inst))
                        return mBlocks[i];
                }
                return null;
            }

            private void WriteFile()
            {
                var tmp = $"{mFilePath}.tmp";
                var folder = NativeFileSystem.GetFolderName(mFilePath);
                NativeFileSystem.EnsureDirectory(folder);
                var buffer = new byte[1024];
                var stream = NativeFileSystem.OpenFile(tmp, FileMode.Create, FileAccess.ReadWrite);
                var zip = new ZipOutputStream(stream);
                if(!string.IsNullOrEmpty(mSys.mPwd))
                {
                    zip.Password = mSys.mPwd;
                }
                zip.SetLevel(7);
                var dt = mMeta is IJsonSerializable ser ? ser.OnSerialize() : null;
                if (dt != null)
                {
                    var bytes = Encoding.UTF8.GetBytes(dt.ToJson());
                    var entry = new ZipEntry("meta");
                    zip.PutNextEntry(entry);
                    zip.Write(bytes, 0, bytes.Length);
                }
                for (int i = 0; i < mBlocks.Count; i++)
                {
                    var block = mBlocks[i];
                    //var type = block.block.GetType();
                    var mod = new JsonData();
                    mod["ver"] = block.block.Version;
                    mod["data"] = block.serializedDatas;
                    mod["refs"] = block.serializedRefs;
                    var bytes = Encoding.UTF8.GetBytes(mod.ToJson());
                    var entry = new ZipEntry($"DT_{block.block.BlockName}");
                    zip.PutNextEntry(entry);
                    zip.Write(bytes, 0, bytes.Length);
                }
                var zipFolder = new ZipEntry("bin/");
                zip.PutNextEntry(zipFolder);
                foreach(var f in mBinaryFiles.Values)
                {
                    var fname = f.InstanceId.ToString("X");
                    var ext = f.FileType;
                    if (!string.IsNullOrEmpty(ext) && ext[0] != '.')
                        fname = $"bin/{fname}.{ext}";
                    else
                        fname = $"bin/{fname}{ext}";
                    var len = f.GetBytes(ref buffer);
                    if (len > 0)
                    {
                        var entry = new ZipEntry(fname);
                        zip.PutNextEntry(entry);
                        zip.Write(buffer, 0, len);
                    }
                }
                zip.Flush();
                zip.Close();
                zip.Dispose();
                if (NativeFileSystem.HasFile(mFilePath))
                    NativeFileSystem.DeleteFile(mFilePath);
                NativeFileSystem.MoveFile(tmp, mFilePath);
                RTLog.Log(LogCat.Persistent, $"saved file: {mFilePath}");
            }
        }
    }
}
